package spring.cloud.tasks.tasks_manager.schedule;

import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import spring.cloud.tasks.common.DateUtils;
import spring.cloud.tasks.common.SuspendableCronExpression;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;


@Setter
@Getter
public class TaskFireDateTimeAndPreAssignDateTimeListHolder {
    //
    private String taskType;
    private final String taskId;

    /**
     * 注意生产环境，执行频率一般不会进行改动。
     * 在测试环境，一般情况都是大的往小了改。比如一天执行一次，改成5分钟或者10分钟执行一次。这种场景比较多。
     */
    private volatile SuspendableCronExpression suspendableCronExpression;
    private long fireDateTimeVersion;
    /**
     * 提前进行预分配时间:单位分钟
     */
    private volatile Integer preAssignMinutes;
    //
    private volatile List<FireDateTimeAndPreAssignDateTime> fireDateTimeAndPreAssignDateTimeList = new ArrayList<FireDateTimeAndPreAssignDateTime>();
    private volatile Date computeNextFireTimeMarkDate = new Date(0);//init
    //
    private volatile Date lastPreAssignDate;
    private volatile Date lastRealPreAssignDate;
    /**
     * 可能在调度器启动的1分钟内，处理比较忙。但是12分钟后，则创建任务实例会变得非常平稳。
     */
    public static final int $12_value = 12;//12分钟是半轮回
    public static final int $24_value = 24;//24分钟是一个轮回

    public TaskFireDateTimeAndPreAssignDateTimeListHolder(String taskId) {
        this.taskId = taskId;
    }

    public void compute(@NonNull long fireDateTimeVersion, @NonNull String cronExpression, String pauseDatePeriod, String pauseTimePeriod, Integer preAssignMinutes) {
        this.fireDateTimeVersion = fireDateTimeVersion;
        if (preAssignMinutes == null || preAssignMinutes < 1) {
            preAssignMinutes = 1;
        }
        this.suspendableCronExpression = new SuspendableCronExpression(fireDateTimeVersion, cronExpression, pauseDatePeriod, pauseTimePeriod);
        this.preAssignMinutes = preAssignMinutes;
        List<FireDateTimeAndPreAssignDateTime> fireDateTimeAndPreAssignDateTimeList = compute();
        synchronized (this) {//引用对象不保证可见性
            this.fireDateTimeAndPreAssignDateTimeList = fireDateTimeAndPreAssignDateTimeList;//快速替换对象 那么不会出现执行的时间频率变化很大的情况出现
            this.computeNextFireTimeMarkDate = new Date();
        }
    }

    /**
     * 计算的时候按照1分钟以前进行计算。避免执行点在当前时间附近遗漏，在生成实例的时候需要保证执行点尽可能的在未来。
     * <pre>
     * >1经常用到的时间频率: 1分钟，3分钟，5分钟，10，15，30分钟，1个小时，2个小时，5个小时。。。间隔24小时。或者几天或者月末才进行调度一下。
     * >2预处理执行点要求:4==正在执行+预分频+待加入到预分配队列+候选。当然这是理想化的。
     * >3实例生成和持久化和加载机制: 距离执行开始时间点16分钟就需要强制实例信息:
     *  1预分配的时间是[1/2]分钟
     *  2任务实例信息必须保证在[5,6]分钟内在加载到内存
     *  3(6，16]内的实例信息必须生成并持久化到数据库。
     *  4生成的实例需要马上进行对比，如果在DB或者内存已经存在了则什么也不做，如果不存在那么增量写入。
     * >4执行频率也很重要
     * 如果执行的频率比较高。在半个小时内就执行了好多次。那么先生成执行计划，反而会减少性能开销。注意16分钟必须生成任务实例且报错任务实例信息到DB。
     * 如果执行的频率比较低。可能几个小时才执行一次，有可能一天才跑一次批。像这种的。不用忙生成任务实例。只需要记录下次的执行时间点。任务实例执行点在30分钟内就需要开始生成任务实例信息且保持DB。(对应间隔比较长的情况)
     * </pre>
     */
    public List<FireDateTimeAndPreAssignDateTime> compute() {
        //
        List<FireDateTimeAndPreAssignDateTime> fireDateTimeAndPreAssignDateTimeList = new ArrayList<FireDateTimeAndPreAssignDateTime>();
        long nowDateTime = System.currentTimeMillis();
        Date startDate = new Date(nowDateTime - DateUtils.MINUTE * 1);//可能在首次启动计算的时候时间比较紧
        Date nextFireTime = startDate;
        int count = 0;
        while (true) {
            nextFireTime = this.suspendableCronExpression.getNextFireTimeAfter(nextFireTime);
            if (nextFireTime.getTime() < nowDateTime) {
                continue;
            }
            FireDateTimeAndPreAssignDateTime dateTimeAndPreAssignDateTime = new FireDateTimeAndPreAssignDateTime(taskType, taskId, nextFireTime, new Date(nextFireTime.getTime() - 1000 * 60 * preAssignMinutes.intValue()), this.suspendableCronExpression.getVersion());
            //如果是晚上定点执行的 那么一天可能只会有一个任务实例。也就是说产生实例提前最多是24分钟
            if (dateTimeAndPreAssignDateTime.getNextFireDate().getTime() > nowDateTime + DateUtils.MINUTE * $24_value) {
                return fireDateTimeAndPreAssignDateTimeList;
            }
            fireDateTimeAndPreAssignDateTimeList.add(dateTimeAndPreAssignDateTime);
            count++;
            //必须提前12分钟创建任务实例，
            if (nextFireTime.getTime() < nowDateTime + DateUtils.MINUTE * $12_value) {
                continue;
            } else {
                //时间大于等于12分钟 只要出现3个任务实例。或者下次的任务实例的执行时间点位于24分钟后，那么就不再新的任务实例。
                if (count >= 3) {
                    break;
                } else {
                    continue;
                }
            }
        }
        return fireDateTimeAndPreAssignDateTimeList;
    }
}
